import bson
import motor
import tornado.escape
import tornado.ioloop
import tornado.locks
import tornado.platform.asyncio
import tornado.web
from pymongo import ReturnDocument
from tornado.options import define, options

import status

define('port', default='8000', type=int, help='run at given port')

MONGODB_URI = 'mongodb://localhost:27017/tornado_db2'
PER_PAGE = 10


# mysql:    数据库(database) =》 数据表(table)=》 数据行(row)
# mongoddb: 数据库(database) =》 集合(collection) =》 文档(document)
class BaseHandler(tornado.web.RequestHandler):
    @property
    def mongo_db(self):
        # return self.application.settings['mongo_db']
        return self.settings['mongo_db']

    def get_collection(self, collection_name):
        return self.mongo_db[collection_name]

    # get方法调用之前执行这个方法,获取前端传来的JSON数据
    def prepare(self):
        """
        把用户上传的JSON数据，赋值给self.request_data属性中
        :return: None
        """
        self.request_data = {}
        # Content-Type可能有两种形式 application/json或application/json; charset=UTF-8;
        # 因此这里使用get默认空字符串和startswith方法
        is_json = self.request.headers.get('Content-Type', '').startswith('application/json')
        # JSON数据也可能为空对象
        if self.request.body and is_json:
            self.request_data = tornado.escape.json_decode(self.request.body)

    def object_id(self, post_id):
        """
        把字符串ID转换成ObjectId对象,如果post_id格式不符合要求返回HTTP 400错误
        :param post_id: "5f45f171a78b0b06bf6fcce83"
        :return: ObjectId("5f45f171a78b0b06bf6fcce83")
        """
        try:
            object_id = bson.ObjectId(post_id)
        # 用户可能传来无效的post_id
        except bson.errors.InvalidId:
            raise tornado.web.HTTPError(status.HTTP_400_BAD_REQUEST)
        else:
            return object_id

    def set_default_headers(self):
        """设置默认相应头内容类型(Content-Type)为JSON"""
        self.set_header('Content-Type', 'application/json; charset="utf-8"')

class Pagination:
    def __init__(self, collection, per_page):
        self.collection = collection
        self.per_page = per_page

    async def paginate(self, page):
        try:
            page = int(page)
        except (TypeError, ValueError):
            page = 1
        except Exception:
            raise tornado.web.HTTPError(status.HTTP_404_NOT_FOUND)

        # per_page = 10

        count = await self.collection.count_documents({})
        total_page, remainder = divmod(count, self.per_page)
        if remainder:
            total_page += 1

        if page <= 0:
            page = 1

        if page > total_page:
            page = total_page

        skip = (page - 1) * self.per_page

        cursor = self.collection.find({})
        # cursor.sort('-pub_date').skip(skip).limit(per_page)
        cursor.skip(skip).limit(self.per_page)
        documents = [document async for document in cursor]
        return documents

class PostListHandler(BaseHandler):
    # 仅支持GET和POST方法，其他方法返回HTTP 405: Method Not Allowed
    SUPPORTED_METHODS = ('GET', 'POST')

    async def get(self):
        """获取文章列表"""

        # http://127.0.0.1:8000/api/v1/posts/?page=3
        page = self.get_argument('page', default='1')   # default: Union[None, str, _ArgDefaultMarker] int会警告
        pagination = Pagination(self.get_collection('posts'), PER_PAGE)
        documents = await pagination.paginate(page)

        # cursor = self.get_collection('posts').find({})
        # documents = [document for document in await cursor.to_list(length=100)]
        for document in documents:
            document['_id'] = str(document['_id'])  # 把ObjectId对象转换为str
        self.set_status(status.HTTP_200_OK)
        self.write({'status': 0, 'results': documents})

    # 创建数据 - 要求前端一定要上传的是JSON数据并指定请求头Content-Type: application/json
    async def post(self):
        """以POST方式上传文章"""
        title = self.request_data.get('title')
        content = self.request_data.get('content')
        document = {'title': title, 'content': content}
        result = await self.get_collection('posts').insert_one(document)
        self.set_status(status.HTTP_201_CREATED)
        self.write({'status': 0, 'inserted_id': str(result.inserted_id)})


class PostDetailHandler(BaseHandler):
    # 仅支持GET, PATCH和DELETE方法，其他方法返回HTTP 405: Method Not Allowed
    SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE')

    async def get(self, post_id):
        """获取单个文章"""
        document = await self.get_collection('posts').find_one({'_id': self.object_id(post_id)})
        if document is None:
            raise tornado.web.HTTPError(status.HTTP_404_NOT_FOUND)
        document['_id'] = str(document['_id'])
        self.set_status(status.HTTP_200_OK)
        self.write({'status': 0, 'result': document})

    async def patch(self, post_id):
        """修改文章"""
        title = self.request_data.get('title')
        content = self.request_data.get('content')
        new_document = {}
        # 不直接使用new_document = {'title': title, 'content': content}
        # 防止只更新title或content时,content或title更新成null
        if title:
            new_document['title'] = title
        if content:
            new_document['content'] = content

        # 如果用户上传的使用空的JSON数据，抛异常
        if not new_document:
            raise tornado.web.HTTPError(status.HTTP_400_BAD_REQUEST, log_message="标题或内容不能为空")

        # return_document选项返回更新后的文档，默认返回旧的文档
        document = await self.get_collection('posts').find_one_and_update(
            {'_id': self.object_id(post_id)},
            {'$set': new_document},
            return_document=ReturnDocument.AFTER
        )

        if document is None:
            raise tornado.web.HTTPError(status.HTTP_404_NOT_FOUND)

        document['_id'] = str(document['_id'])
        self.set_status(status.HTTP_200_OK)
        self.write({'status': 0, 'result': document})

    async def delete(self, post_id):
        """删除文章"""
        document = await self.get_collection('posts').find_one_and_delete(
            {'_id': self.object_id(post_id)}
        )

        if document is None:
            raise tornado.web.HTTPError(status.HTTP_404_NOT_FOUND)

        document['_id'] = str(document['_id'])
        self.write({'status': 0, 'result': document})


def make_app(settings):
    return tornado.web.Application([
        (r'/api/v1/posts/', PostListHandler),
        (r'/api/v1/posts/(\w+)/', PostDetailHandler),
    ], **settings)


def main():
    tornado.options.parse_command_line()

    # 配置tornado使用asyncio事件循环
    tornado.platform.asyncio.AsyncIOMainLoop().install()
    ioloop = tornado.ioloop.IOLoop.current()

    # 连接本地数据库，数据库名为: tornado_db
    motor_client = motor.motor_tornado.MotorClient(MONGODB_URI)
    mongo_db = motor_client.get_default_database()

    app = make_app({
        'debug': True,
        'mongo_db': mongo_db
    })
    app.listen(options.port)

    ioloop.start()


if __name__ == "__main__":
    main()
